package uk.ac.lkl.common.associator.ui;



import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.*;

import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import uk.ac.lkl.common.associator.Associator;
import uk.ac.lkl.common.associator.event.AssociatorAdapter;
import uk.ac.lkl.common.associator.event.AssociatorEvent;
import uk.ac.lkl.common.ui.HoverTracker;
import uk.ac.lkl.common.ui.ViewCreator;
import uk.ac.lkl.common.util.ds.*;
import uk.ac.lkl.common.util.ds.event.*;


public class NewAssociatorView extends JPanel {

    private ListListener listListener = new ListListener() {

        public void elementAdded(ListEvent e) {
            processElementAdded(e);
        }


        public void elementMoved(ListEvent e) {
            processElementMoved(e);
        }


        public void elementRemoved(ListEvent e) {
            processElementRemoved(e);
        }
    };


    private HoverTracker<AssociationViewPanel> hoverTracker;


    private AssociationViewPanelLinker panelLinker;


    private DeleteListener deleteListener;


    private GridBagLayout layout;


    private GridBagConstraints c = new GridBagConstraints();


    /**
     * A map from each object to its corresponding panel.
     * 
     */
    private HashMap<Object, AssociationViewPanel> modelToView = new HashMap<Object, AssociationViewPanel>();


    /**
     * The classes in this view.
     * 
     */
    private HashMap<Class, Integer> classToColumn;


    private HashMap<Class, AbstractNotifyingList> lists;


    /**
     * A map from class to its corresponding view creator.
     * 
     * This is used in response to collection events to create new interface
     * components for the newly-created elements.
     * 
     */
    private HashMap<Class, ViewCreator> viewCreatorMap;


    /**
     * The set of associators in this view.
     * 
     * Currently there is no intelligent handling of related classes. e.g. A<->B
     * followed by B<->C.
     * 
     */
    private ArrayList<Associator> associators;


    /**
     * The set of links for this instance.
     * 
     */
    private ArrayList<AssociationViewLink> links = new ArrayList<AssociationViewLink>();


    // private AssociationViewLink highlightedLink;

    private LinkHighlighter linkHighlighter;


    /**
     * Create a new instance.
     * 
     */
    public NewAssociatorView() {
        this.classToColumn = new HashMap<Class, Integer>();
        this.viewCreatorMap = new HashMap<Class, ViewCreator>();
        this.associators = new ArrayList<Associator>();
        this.lists = new HashMap<Class, AbstractNotifyingList>();
        initialiseHoverTracker();
        initialisePanelLinker();
        initialiseDeleteListener();
        initialiseLinkHighlighter();
        initialiseLayout();

        addMouseHandler();
    }


    /**
     * Create a new instance consisting of an initial associator.
     * 
     * This is equivalent to the empty constructor call followed by a call to
     * <code>addAssociator</code>.
     * 
     * @param <L>
     *            the left element type
     * @param <A>
     *            the annotation type
     * @param <R>
     *            the right element type
     * @param associator
     *            the associator
     * @param leftViewCreator
     *            the view creator for left elements
     * @param rightViewCreator
     *            the view creator for right elements
     * 
     */
    public <L, A, R> NewAssociatorView(Associator<L, A, R> associator,
            ViewCreator<L> leftViewCreator, ViewCreator<R> rightViewCreator) {
        this();
        addAssociator(associator, leftViewCreator, rightViewCreator);
    }


    private void initialiseHoverTracker() {
        hoverTracker = new HoverTracker<AssociationViewPanel>();
        hoverTracker.addChangeListener(new ChangeListener() {

            public void stateChanged(ChangeEvent e) {
                repaint();
            }
        });
    }


    private void initialisePanelLinker() {
        panelLinker = new AssociationViewPanelLinker(this);
    }


    private void initialiseDeleteListener() {
        deleteListener = new DeleteListener(this);
    }


    private void initialiseLinkHighlighter() {
        linkHighlighter = new LinkHighlighter(this);
    }


    /**
     * Add the given associator.
     * 
     * @param <L>
     *            the left element type
     * @param <A>
     *            the annotation type
     * @param <R>
     *            the right element type
     * @param associator
     *            the associator between L and R with annotations A
     * @param leftViewCreator
     *            the view creator for instances of L. Can be null if view
     *            creator for L already set.
     * @param rightViewCreator
     *            the view creator for instances of R. Can be null if view
     *            creator for R already set.
     * 
     */
    public <L, A, R> void addAssociator(Associator<L, A, R> associator,
                                        ViewCreator<L> leftViewCreator,
                                        ViewCreator<R> rightViewCreator) {
        associators.add(associator);

        lists.put(associator.getLeftClass(), associator.getLeftList());
        lists.put(associator.getRightClass(), associator.getRightList());

        AbstractNotifyingList<L> leftList = associator.getLeftList();
        AbstractNotifyingList<R> rightList = associator.getRightList();

        leftList.addListListener(listListener);
        rightList.addListListener(listListener);

        registerClasses(associator);
        setViewCreators(associator, leftViewCreator, rightViewCreator);
        addListeners(associator);
        createViews(associator, leftViewCreator, rightViewCreator);
        processAssociations(associator);
    }


    private void processElementAdded(ListEvent e) {
        AbstractNotifyingList list = e.getList();
        Class elementClass = list.getElementClass();

        Object model = e.getElement();

        ViewCreator viewCreator = viewCreatorMap.get(elementClass);

        // allow for header row
        int rowIndex = e.getNewIndex() + 1;

        int columnIndex = classToColumn.get(elementClass);

        // hack column for moment
        createViewPanel(model, elementClass, viewCreator, rowIndex, columnIndex);
        validate();
    }


    private void processElementMoved(ListEvent e) {
        Object model = e.getElement();
        AssociationViewPanel viewPanel = modelToView.get(model);
        GridBagConstraints constraints = layout.getConstraints(viewPanel);
        int newIndex = e.getNewIndex();
        constraints.gridy = newIndex + 1;
        layout.setConstraints(viewPanel, constraints);
        viewPanel.invalidate();
        validate();

        // hack
        getParent().repaint();
    }


    private void processElementRemoved(ListEvent e) {
        Object model = e.getElement();
        AssociationViewPanel viewPanel = modelToView.get(model);
        remove(viewPanel);
        invalidate();
        validate();
        repaint();
    }


    public <O> AbstractNotifyingList<O> getList(Class<O> elementClass) {
        return lists.get(elementClass);
    }


    /**
     * 
     * Needs to be updated so adds at appropriate column.
     * 
     * @param <L>
     *            the left element type
     * @param <A>
     *            the annotation type
     * @param <R>
     *            the right element type
     * @param associator
     *            the associator
     * @param leftViewCreator
     *            the view creator for left elements
     * @param rightViewCreator
     *            the view creator for right elements
     * 
     */
    private <L, A, R> void createViews(Associator<L, A, R> associator,
                                       ViewCreator<L> leftViewCreator,
                                       ViewCreator<R> rightViewCreator) {
        AbstractNotifyingList<L> leftList = associator.getLeftList();
        Class<L> leftClass = leftList.getElementClass();
        int leftListColumn = classToColumn.get(leftClass);
        layoutListComponents(leftList, leftViewCreator, leftListColumn);

        c.gridx = 1;
        c.gridy = 0;
        JPanel gapPanel = new JPanel();
        gapPanel.setPreferredSize(new Dimension(200, 10));
        gapPanel.setMinimumSize(new Dimension(200, 10));
        gapPanel.setOpaque(false);
        add(gapPanel, c);

        AbstractNotifyingList<R> rightList = associator.getRightList();
        Class<R> rightClass = rightList.getElementClass();
        int rightListColumn = classToColumn.get(rightClass);

        layoutListComponents(rightList, rightViewCreator, rightListColumn);
    }


    /**
     * Layout the elements of the given list.
     * 
     * @param <O>
     *            the element type
     * @param list
     *            the list of elements
     * @param viewCreator
     *            the view creator
     * @param columnIndex
     *            the column in which to place these elements
     * 
     */
    private <O> void layoutListComponents(AbstractNotifyingList<O> list,
                                          ViewCreator<O> viewCreator,
                                          int columnIndex) {
        for (int i = 0; i < list.size(); i++) {
            O model = list.get(i);
            int rowIndex = i + 1; // allow for header

            // can simplify this once have map from class to column
            createViewPanel(
                            model,
                            list.getElementClass(),
                            viewCreator,
                            rowIndex,
                            columnIndex);
        }
    }


    private <O> void createViewPanel(O model,
                                     Class<O> modelClass,
                                     ViewCreator<O> viewCreator,
                                     int rowIndex,
                                     int columnIndex) {
        AssociationViewPanel<O> viewPanel = new AssociationViewPanel<O>(model,
                modelClass, viewCreator);

        addViewPanelListeners(viewPanel);
        addView(viewPanel, rowIndex, columnIndex);
        modelToView.put(model, viewPanel);
    }


    public void associate(AssociationViewPanel source,
                          AssociationViewPanel destination) {
        Class sourceClass = source.getModelClass();
        Class destinationClass = destination.getModelClass();

        // look through associators to see where can add
        for (Associator associator : associators) {
            Class leftClass = associator.getLeftClass();
            Class rightClass = associator.getRightClass();

            if (sourceClass == leftClass && destinationClass == rightClass)
                associator.addAssociation(source.getModel(), destination
                        .getModel());
            else if (sourceClass == rightClass && destinationClass == leftClass)
                associator.addAssociation(destination.getModel(), source
                        .getModel());
        }
    }


    /**
     * Add the given view at a given row and column in this instance's
     * GridBagLayout.
     * 
     * This method wraps the component in a border. Currently no check is made
     * as to whether the component already has a border or not.
     * 
     * @param view
     * @param row
     * @param column
     * 
     */
    private <O> void addView(AssociationViewPanel<O> associationViewPanel,
                             int row,
                             int column) {
        c.gridx = column;
        c.gridy = row;
        c.weightx = 0.0;
        add(associationViewPanel, c);
    }


    private <L, A, R> void registerClasses(Associator<L, A, R> associator) {
        Class<L> leftClass = associator.getLeftClass();
        Class<R> rightClass = associator.getRightClass();

        // hack for columns
        classToColumn.put(leftClass, 0);
        classToColumn.put(rightClass, 2);
    }


    public List<AssociationViewLink> getLinks() {
        return Collections.unmodifiableList(links);
    }


    private <L, A, R> void setViewCreators(Associator<L, A, R> associator,
                                           ViewCreator<L> leftViewCreator,
                                           ViewCreator<R> rightViewCreator) {
        setViewCreator(associator.getLeftClass(), leftViewCreator);
        setViewCreator(associator.getRightClass(), rightViewCreator);
    }


    /**
     * Set the view creator for the specified element class.
     * 
     * This method does nothing if the viewCreator is <code>null</code>.
     * 
     * Note that currently this method does not retrospectively change all
     * existing views of the given class.
     * 
     * @param <E>
     *            the element type
     * @param elementClass
     *            the element class
     * @param viewCreator
     *            the view creator
     * 
     */
    public <E> void setViewCreator(Class<E> elementClass,
                                   ViewCreator<E> viewCreator) {
        if (viewCreator == null)
            return;

        ViewCreator<E> oldViewCreator = (ViewCreator<E>) viewCreatorMap
                .put(elementClass, viewCreator);
        if (oldViewCreator == viewCreator)
            return;

        // update existing views
    }


    /**
     * Add listeners to the associator (and its lists).
     * 
     * This adds a listener to the associator itself so that links can be
     * maintained appropriately. It also adds a collection listener to both of
     * the associator's lists so that components can be created in response to
     * list updates.
     * 
     * @param <L>
     * @param <A>
     * @param <R>
     * @param associator
     * 
     */
    private <L, A, R> void addListeners(Associator<L, A, R> associator) {
        addAssociatorListener(associator);
        addListListener(associator);
    }


    /**
     * Add a listener to the given associator listener.
     * 
     * The listener chains on calls to the appropriate processing methods.
     * 
     * @param <L>
     *            the left element type
     * @param <A>
     *            the annotation type
     * @param <R>
     *            the right element type
     * @param associator
     *            the associator
     * 
     */
    private <L, A, R> void addAssociatorListener(Associator<L, A, R> associator) {
        associator.addAssociatorListener(new AssociatorAdapter<L, A, R>() {

            public void associationAdded(AssociatorEvent<L, A, R> e) {
                processAssociationAdded(e);
            }


            public void associationRemoved(AssociatorEvent<L, A, R> e) {
                processAssociationRemoved(e);
            }
        });
    }


    /**
     * Process a new association as a result of an AssociatorEvent.
     * 
     * This digs out the details of the event and chains the call.
     * 
     * @param e
     *            the associator event
     * 
     */
    private <L, A, R> void processAssociationAdded(AssociatorEvent<L, A, R> e) {
        Associator<L, A, R> associator = e.getAssociator();
        L leftElement = e.getLeftElement();
        A annotation = e.getAnnotation();
        R rightElement = e.getRightElement();
        processAssociationAdded(
                                associator,
                                leftElement,
                                annotation,
                                rightElement);
    }


    /**
     * Process a new association.
     * 
     * @param <L>
     *            the left element type
     * @param <A>
     *            the annotation type
     * @param <R>
     *            the right element type
     * @param associator
     *            the associator
     * @param leftElement
     *            the left element
     * @param annotation
     *            the annotation
     * @param rightElement
     *            the right element
     * 
     */
    private <L, A, R> void processAssociationAdded(Associator<L, A, R> associator,
                                                   L leftElement,
                                                   A annotation,
                                                   R rightElement) {
        AssociationViewPanel leftViewPanel = modelToView.get(leftElement);
        AssociationViewPanel rightViewPanel = modelToView.get(rightElement);
        AssociationViewLink link = new AssociationViewLink(leftViewPanel,
                rightViewPanel);
        links.add(link);
        repaint();
    }


    /**
     * Process the removal of an association as the result of an
     * <code>AssociatorEvent</code>.
     * 
     * @param <L>
     *            the left element type
     * @param <A>
     *            the annotation type
     * @param <R>
     *            the right annotation type
     * @param e
     *            the associator event
     * 
     */
    private <L, A, R> void processAssociationRemoved(AssociatorEvent<L, A, R> e) {
        Associator<L, A, R> associator = e.getAssociator();
        L leftElement = e.getLeftElement();
        A annotation = e.getAnnotation();
        R rightElement = e.getRightElement();
        processAssociationRemoved(
                                  associator,
                                  leftElement,
                                  annotation,
                                  rightElement);
    }


    /**
     * Process the removal of an association.
     * 
     * @param <L>
     *            the left element type
     * @param <A>
     *            the annotation type
     * @param <R>
     *            the right element type
     * @param associator
     *            the associator
     * @param leftElement
     *            the left element
     * @param annotation
     *            the annotation
     * @param rightElement
     *            the right element
     * 
     */
    private <L, A, R> void processAssociationRemoved(Associator<L, A, R> associator,
                                                     L leftElement,
                                                     A annotation,
                                                     R rightElement) {
        AssociationViewPanel sourcePanel = modelToView.get(leftElement);
        AssociationViewPanel destinationPanel = modelToView.get(rightElement);

        AssociationViewLink link = new AssociationViewLink(sourcePanel,
                destinationPanel);

        boolean removedOk = links.remove(link);
        repaint();
    }


    /**
     * Add list listeners to the given associator.
     * 
     * This adds a <code>ListListener</code> to both the left and right list
     * of the associator so that list events can modify the interface
     * appropriately.
     * 
     * @param <L>
     *            the left element type
     * @param <A>
     *            the annotation type
     * @param <R>
     *            the right element type
     * @param associator
     *            the associator
     * 
     */
    private <L, A, R> void addListListener(Associator<L, A, R> associator) {
        AbstractNotifyingList<L> leftList = associator.getLeftList();
        AbstractNotifyingList<R> rightList = associator.getRightList();
        // todo: add list listeners
    }


    /**
     * Process the associations as specified in the associator.
     * 
     * This is called when an associator is first added to the view so that all
     * existing links are created in the view as appropriate.
     * 
     * @param <L>
     *            the left element type
     * @param <A>
     *            the annotation type
     * @param <R>
     *            the right element type
     * @param associator
     *            the associator to process;
     * 
     */
    private <L, A, R> void processAssociations(Associator<L, A, R> associator) {
        for (L leftElement : associator.getLeftList()) {
            ArrayList<R> rightElements = associator
                    .getRightAssociations(leftElement);

            if (rightElements == null)
                continue;

            for (R rightElement : rightElements) {
                A annotation = associator.getAnnotation(
                                                        leftElement,
                                                        rightElement);
                processAssociationAdded(
                                        associator,
                                        leftElement,
                                        annotation,
                                        rightElement);
            }
        }
    }


    /**
     * Initialise the layout of this instance.
     * 
     */
    private void initialiseLayout() {
        layout = new GridBagLayout();
        setLayout(layout);
        c.fill = GridBagConstraints.BOTH;
    }


    private void addViewPanelListeners(AssociationViewPanel viewPanel) {
        hoverTracker.addHoverComponent(viewPanel);
        panelLinker.addViewPanel(viewPanel);
        deleteListener.addViewPanel(viewPanel);
    }


    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        drawLinks(g);
        panelLinker.paint(g);
    }


    /**
     * Get the panels that the specified view panel can validly link to.
     * 
     * @param viewPanel
     * @return
     * 
     */
    public ArrayList<AssociationViewPanel> getLinkableComponents(AssociationViewPanel viewPanel) {
        // todo: maintain cache of this data. Update when list
        // contents change

        // policy on what can link to what is implemented here

        ArrayList<AssociationViewPanel> linkableComponents = new ArrayList<AssociationViewPanel>();

        Class modelClass = viewPanel.getModelClass();

        // for moment, target models are all those that aren't of
        // given class
        for (Map.Entry<Class, AbstractNotifyingList> entry : lists.entrySet()) {
            Class listClass = entry.getKey();
            if (modelClass == listClass)
                continue;

            AbstractNotifyingList list = entry.getValue();
            for (Object model : list) {
                AssociationViewPanel view = modelToView.get(model);
                linkableComponents.add(view);
            }
        }

        return linkableComponents;
    }


    private void drawLinks(Graphics g) {
        Graphics2D g2 = (Graphics2D) g.create();

        AssociationViewPanel highlightedViewPanel = hoverTracker
                .getHoverComponent();
        if (highlightedViewPanel != null)
            paintHighlights(g2, highlightedViewPanel);

        for (AssociationViewLink link : links) {
            AssociationViewPanel sourcePanel = link.getSource();
            AssociationViewPanel destinationPanel = link.getDestination();

            Rectangle leftBounds = sourcePanel.getBounds();
            Rectangle rightBounds = destinationPanel.getBounds();

            int x1 = leftBounds.x + leftBounds.width;
            int y1 = leftBounds.y + leftBounds.height / 2;
            int x2 = rightBounds.x;
            int y2 = rightBounds.y + rightBounds.height / 2;
            g2.setColor(Color.black);

            if (link == linkHighlighter.getHighlightedLink())
                g2.setStroke(new BasicStroke(2.0f));
            else
                g2.setStroke(new BasicStroke(0.5f));

            g2.drawLine(x1, y1, x2, y2);
        }
    }


    public void addMouseHandler() {
        addMouseListener(new MouseAdapter() {

            public void mousePressed(MouseEvent e) {
                processMouseClicked(e);
            }
        });
    }


    private void processMouseClicked(MouseEvent e) {
        if (e.getClickCount() < 2)
            return;

        int x = e.getX();
        int y = e.getY();

        Double minimumDistance = null;
        AssociationViewLink minimumLink = null;

        for (AssociationViewLink link : links) {
            AssociationViewPanel leftPanel = link.getSource();
            AssociationViewPanel rightPanel = link.getDestination();

            Rectangle leftBounds = leftPanel.getBounds();
            Rectangle rightBounds = rightPanel.getBounds();

            int x1 = leftBounds.x + leftBounds.width;
            int y1 = leftBounds.y + leftBounds.height / 2;
            int x2 = rightBounds.x;
            int y2 = rightBounds.y + rightBounds.height / 2;

            // should cache these
            Line2D.Double line = new Line2D.Double(x1, y1, x2, y2);

            // hack for horizontal layout
            if (x < x1 || x > x2)
                continue;

            double distance = line.ptLineDistSq(x, y);

            if (minimumDistance == null) {
                minimumDistance = distance;
                minimumLink = link;
            }
            else {
                if (distance < minimumDistance) {
                    minimumDistance = distance;
                    minimumLink = link;
                }
            }
        }

        if (minimumLink == null)
            return;

        // 10 pixels
        if (minimumDistance > 100)
            return;

        AssociationViewPanel leftPanel = minimumLink.getSource();
        Object leftObject = leftPanel.getModel();
        AssociationViewPanel rightPanel = minimumLink.getDestination();
        Object rightObject = rightPanel.getModel();

        // hack: for now, only uses first associator
        associators.get(0).removeAssociation(leftObject, rightObject);
    }


    // draw highlights
    private void paintHighlights(Graphics2D g2,
                                 AssociationViewPanel highlightedViewPanel) {

        for (AssociationViewLink link : links) {
            AssociationViewPanel sourcePanel = link.getSource();
            AssociationViewPanel destinationPanel = link.getDestination();

            if (sourcePanel != highlightedViewPanel
                    && destinationPanel != highlightedViewPanel)
                continue;

            Rectangle leftBounds = sourcePanel.getBounds();
            Rectangle rightBounds = destinationPanel.getBounds();

            int x1 = leftBounds.x + leftBounds.width;
            int y1 = leftBounds.y + leftBounds.height / 2;
            int x2 = rightBounds.x;
            int y2 = rightBounds.y + rightBounds.height / 2;
            g2.setColor(Color.yellow);

            g2.setStroke(new BasicStroke(4f));

            g2.drawLine(x1, y1, x2, y2);
        }
    }


    /**
     * Get the associator at the given index.
     * 
     * @param index
     *            the index
     * @return the associator
     * 
     */
    public Associator getAssociator(int index) {
        return associators.get(index);
    }

}
